Перейти к основному содержимому

5.02. ООП

Разработчику Архитектору

ООП

Объектно-ориентированное программирование (ООП)
Особенности ООП в Python
Основы: классы и объекты.
Методы
Поля и свойства
Инкапсуляция: _, __ (name mangling).
Наследование: базовый и расширенный классы.
Полиморфизм: переопределение методов, duck typing.
Абстракция: ABC (Abstract Base Classes).
Магические методы (__init__, __str__, __repr__, __call__, __enter__, __exit__).
Пример: реализация собственного контейнера.

ООП в Python

Объектно-ориентированное программирование — это парадигма программирования, в основе которой лежит концепция объектов, представляющих собой экземпляры классов. Каждый объект инкапсулирует состояние (данные) и поведение (методы), что позволяет моделировать сущности предметной области с высокой степенью абстракции. ООП опирается на четыре ключевые принципа: инкапсуляцию, наследование, полиморфизм и абстракцию.

В Python ООП реализовано не как строго типизированная система, подобная C# или Java, а как гибкая, динамическая модель, где границы между типами, классами и объектами стерты. Это придаёт Python особую выразительность, но одновременно требует от разработчика более глубокого понимания механизмов языка.

ООП в Python существенно отличается от его реализации в строго типизированных языках, таких как C#. Эти различия обусловлены философией самого языка: duck typing, динамическая типизация, метаклассы, единообразие системы типов и первоклассность функций.

  1. Всё является объектом. В Python всё является объектом: числа, строки, функции, модули, классы. Каждый объект имеет тип, значение и набор атрибутов. Даже сам класс — это объект, экземпляр метакласса (по умолчанию type). Это означает, что классы можно создавать динамически, передавать как аргументы, присваивать переменным.
class MyClass:
pass

print(type(MyClass)) # <class 'type'>
  1. Динамичность и изменяемость. Классы и объекты в Python являются изменяемыми во время выполнения. Можно добавлять, удалять или изменять атрибуты и методы "на лету". Это даёт большую гибкость, но усложняет статический анализ кода.
obj = MyClass()
obj.new_attr = "dynamic"
MyClass.new_method = lambda self: print("Added dynamically")
  1. Отсутствие строгой инкапсуляции. Python не поддерживает настоящих приватных членов. Вместо этого используется соглашение об именовании (_ и __) и механизм name mangling. Это означает, что инкапсуляция — скорее социальный контракт, чем техническое ограничение.

  2. Duck typing вместо строгой типизации. Python не требует явного наследования от интерфейса. Если объект ведёт себя как нужный тип (имеет нужные методы и поведение), он считается совместимым. Это центральный элемент полиморфизма в Python. Если это ходит как утка и крякает как утка, значит, это утка.

  3. Метаклассы. Классы в Python создаются с помощью метаклассов. По умолчанию используется type, но можно определить собственный метакласс для изменения способа создания классов. Это мощный механизм, позволяющий внедрять поведение на уровне определения класса (например, регистрация классов, автоматическое добавление методов).

Класс — это шаблон (чертиж), описывающий структуру и поведение объектов. Он определяет поля (атрибуты), методы и правила их взаимодействия. В Python класс объявляется с помощью ключевого слова class.

class Point:
def __init__(self, x, y):
self.x = x
self.y = y

Класс является вызываемым объектом. Его вызов приводит к созданию нового экземпляра.

Объект (экземпляр) — это конкретный экземпляр класса, имеющий собственное состояние (значения атрибутов) и доступ к поведению, определённому в классе.

p = Point(1, 2)

Состояние объекта хранится в его пространстве имён — словаре __dict__. Каждый объект имеет ссылку на свой класс через атрибут __class__.

Метод — это функция, определённая внутри класса и предназначенная для работы с экземпляром (или самим классом). В Python различают несколько видов методов:

  1. Обычные методы (instance methods). Принимают self в качестве первого аргумента — ссылку на экземпляр. Вызываются от объекта.
def move(self, dx, dy):
self.x += dx
self.y += dy
  1. Методы класса (@classmethod). Принимают cls — ссылку на сам класс. Полезны для альтернативных конструкторов.
@classmethod
def origin(cls):
return cls(0, 0)
  1. Статические методы (@staticmethod). Не принимают ни self, ни cls. Логически связаны с классом, но не зависят от его состояния. По сути — обычные функции, принадлежащие пространству имён класса.
@staticmethod
def distance(p1, p2):
return ((p1.x - p2.x)**2 + (p1.y - p2.y)**2)**0.5

Поля (атрибуты экземпляра и класса).

Атрибуты — это данные, связанные с объектом или классом.

  • Атрибуты экземпляра создаются в __init__ или позже и хранятся в instance.__dict__.
  • Атрибуты класса определяются на уровне класса и разделяются между всеми экземплярами (осторожно с изменяемыми типами!).
class Counter:
count = 0 # атрибут класса

def __init__(self):
Counter.count += 1 # общая статистика
self.id = Counter.count # атрибут экземпляра

Свойства (property) позволяют контролировать доступ к атрибутам через геттеры, сеттеры и делитеры, сохраняя синтаксис обращения к обычному атрибуту.

class Circle:
def __init__(self, radius):
self._radius = radius

@property
def radius(self):
return self._radius

@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value

@property
def area(self):
return 3.14159 * self._radius ** 2

Использование:

c = Circle(5)
print(c.area) # вызов без ()
c.radius = 10 # проверка через сеттер

Свойства особенно важны при необходимости валидации, ленивых вычислений или совместимости с существующим API.

Инкапсуляция

Инкапсуляция — принцип сокрытия внутренней реализации объекта. В Python она реализуется не через ключевые слова (private, protected), а через соглашения и механизм преобразования имён.

Одно подчёркивание (_attr) — указывает, что атрибут является внутренним. Это сигнал другим разработчикам: "не используйте напрямую, поведение может измениться".

Двойное подчёркивание (__attr) — активирует name mangling: имя атрибута преобразуется в _ClassName__attr. Это предотвращает случайное переопределение в подклассах.

class A:
def __init__(self):
self._internal = 42
self.__mangled = "hidden"

class B(A):
def __init__(self):
super().__init__()
self.__mangled = "also hidden in B"

b = B()
print(b._internal) # 42
print(b._A__mangled) # "hidden"
print(b._B__mangled) # "also hidden in B"

Таким образом, __ — не средство защиты, а защита от случайного доступа и конфликтов имён в иерархии наследования.

Наследование

Наследование позволяет одному классу (производному) наследовать атрибуты и методы другого (базового). Это средство повторного использования кода и построения иерархий типов.

class Animal:
def speak(self):
raise NotImplementedError

class Dog(Animal):
def speak(self):
return "Woof!"

Python поддерживает множественное наследование. Порядок разрешения методов определяется алгоритмом C3 linearization, доступным через __mro__ (Method Resolution Order).

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

print(D.__mro__) # D -> B -> C -> A -> object

Ключевая проблема множественного наследования — алмазное наследование. Решается корректным использованием super(), который следует MRO.

class A:
def method(self):
print("A")

class B(A):
def method(self):
super().method()
print("B")

class C(A):
def method(self):
super().method()
print("C")

class D(B, C):
def method(self):
super().method()
print("D")

D().method() # A → C → B → D

Полиморфизм

Полиморфизм — способность объектов с одинаковым интерфейсом вести себя по-разному.

  1. Переопределение методов. Производный класс может переопределять методы базового, обеспечивая специфическую реализацию.
class Shape:
def area(self):
raise NotImplementedError

class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h

def area(self):
return self.w * self.h
  1. Duck typing. В Python полиморфизм основан не на наследовании, а на наличии нужного интерфейса.
def print_area(shape):
print(shape.area()) # работает с любым объектом, у которого есть area()

Это позволяет использовать композицию вместо наследования и достигать большей гибкости.

Абстракция

Абстракция — выделение ключевых характеристик, скрывая детали реализации. В Python абстрактные классы реализуются через модуль abc. Класс становится абстрактным, если содержит хотя бы один абстрактный метод, помеченный декоратором @abstractmethod.

from abc import ABC, abstractmethod

class Drawable(ABC):
@abstractmethod
def draw(self):
pass

@abstractmethod
def bounding_box(self):
pass

class Circle(Drawable):
def draw(self):
print("Drawing circle")

def bounding_box(self):
return (0, 0, 10, 10)

# Drawable() # Ошибка: нельзя создать экземпляр абстрактного класса

ABC позволяют:

  • Формально определить интерфейс.
  • Запретить создание неполных реализаций.
  • Использовать isinstance() для проверки соответствия интерфейсу.

Также можно определять абстрактные свойства и абстрактные классовые методы.

Магические методы (от double underscore) — это методы с именами вида __xxx__, которые определяют поведение объекта при взаимодействии с языковыми конструкциями. Они не предназначены для прямого вызова, а вызываются интерпретатором в ответ на определённые операции.

Основные магические методы:

  • __init__(self, ...) — инициализация экземпляра. Не конструктор! Конструктор — __new__.
  • __new__(cls, ...) — создание экземпляра. Вызывается до __init__. Может возвращать объект другого класса.
  • __str__(self) — строковое представление для пользователя (str(obj)).
  • __repr__(self) — подробное представление для разработчика (repr(obj)). Желательно, чтобы eval(repr(obj)) == obj.
  • __call__(self, ...) — позволяет вызывать объект как функцию.
  • __len__(self) — возвращает длину (len(obj)).
  • __getitem__(self, key) / __setitem__ / __delitem__ — доступ по индексу (obj[key]).
  • __iter__(self) / __next__(self) — итерация.
  • __enter__(self) /__exit__(self, exc_type, exc_val, exc_tb) — контекстный менеджер (with).
  • __eq__(self, other) / __lt__ / __hash__ — сравнение и хеширование.
  • __add__(self, other) / __mul__ и т.д. — перегрузка операторов.
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y

def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)

def __repr__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)

Магические методы — основа протоколов Python: итерируемый, контейнер, вызываемый, численный и т.д.

Рассмотрим реализацию упрощённого словаря с возможностью отслеживания изменений.

from collections.abc import MutableMapping

class TrackedDict(MutableMapping):
def __init__(self, *args, **kwargs):
self._data = dict(*args, **kwargs)
self._changes = []

def __getitem__(self, key):
return self._data[key]

def __setitem__(self, key, value):
action = "update" if key in self._data else "add"
self._changes.append((action, key, value))
self._data[key] = value

def __delitem__(self, key):
old_value = self._data.pop(key)
self._changes.append(("delete", key, old_value))

def __iter__(self):
return iter(self._data)

def __len__(self):
return len(self._data)

def __repr__(self):
return f"TrackedDict({self._data})"

def get_changes(self):
return self._changes.copy()

def clear_changes(self):
self._changes.clear()

Использование:

td = TrackedDict(a=1)
td['b'] = 2
del td['a']
print(td.get_changes()) # [('add', 'b', 2), ('delete', 'a', 1)]

Здесь мы реализовали протокол изменяемого отображения, используя магические методы и наследование от MutableMapping, которое также предоставляет стандартные методы (keys, values, items и т.д.).